Micron Document
baubs git

Node / mirrors / respira / commits / 2de8cd1

Commit 2de8cd12ff7c84a33b4858f71f6c85c08902b433


Parents : ec19426
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-27T13:22:14+01:00

refactor: Extract WorkflowStepper sub-components and utilities

- Move WorkflowStepper into dedicated folder with sub-components
- Extract StepCircle, StepLabel, and StepPopover components
- Create workflowSteps constant for step definitions
- Extract getCurrentStep logic to workflowStepCalculation utility
- Extract getGuideContent logic to workflowGuideContent utility
- Reduce WorkflowStepper.tsx from 487 to 140 lines (71% reduction)

Part of #33: Extract sub-components from large components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes
Diff

diff --git a/src/components/WorkflowStepper.tsx b/src/components/WorkflowStepper.tsx
deleted file mode 100644
index 40c12fe..0000000
--- a/src/components/WorkflowStepper.tsx
+++ /dev/null
@@ -1,487 +0,0 @@
-import { useState, useRef } from "react";
-import { useClickOutside } from "@/hooks";
-import { useShallow } from "zustand/react/shallow";
-import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore";
-import { usePatternStore } from "../stores/usePatternStore";
-import {
- CheckCircleIcon,
- InformationCircleIcon,
- ExclamationTriangleIcon,
-} from "@heroicons/react/24/solid";
-import { MachineStatus } from "../types/machine";
-
-interface Step {
- id: number;
- label: string;
- description: string;
-}
-
-const steps: Step[] = [
- { id: 1, label: "Connect", description: "Connect to machine" },
- { id: 2, label: "Home Machine", description: "Initialize hoop position" },
- { id: 3, label: "Load Pattern", description: "Choose PES file" },
- { id: 4, label: "Upload", description: "Upload to machine" },
- { id: 5, label: "Mask Trace", description: "Trace pattern area" },
- { id: 6, label: "Start Sewing", description: "Begin embroidery" },
- { id: 7, label: "Monitor", description: "Watch progress" },
- { id: 8, label: "Complete", description: "Finish and remove" },
-];
-
-// Helper function to get guide content for a step
-function getGuideContent(stepId: number, machineStatus: MachineStatus) {
- // Return content based on step
- switch (stepId) {
- case 1:
- return {
- type: "info" as const,
- title: "Step 1: Connect to Machine",
- description:
- "To get started, connect to your Brother embroidery machine via Bluetooth.",
- items: [
- "Make sure your machine is powered on",
- "Enable Bluetooth on your machine",
- 'Click the "Connect to Machine" button below',
- ],
- };
-
- case 2:
- return {
- type: "info" as const,
- title: "Step 2: Home Machine",
- description:
- "The hoop needs to be removed and an initial homing procedure must be performed.",
- items: [
- "Remove the embroidery hoop from the machine completely",
- "Press the Accept button on the machine",
- "Wait for the machine to complete its initialization (homing)",
- "Once initialization is complete, reattach the hoop",
- "The machine should now recognize the hoop correctly",
- ],
- };
-
- case 3:
- return {
- type: "info" as const,
- title: "Step 3: Load Your Pattern",
- description:
- "Choose a PES embroidery file from your computer to preview and upload.",
- items: [
- 'Click "Choose PES File" in the Pattern File section',
- "Select your embroidery design (.pes file)",
- "Review the pattern preview on the right",
- "You can drag the pattern to adjust its position",
- ],
- };
-
- case 4:
- return {
- type: "info" as const,
- title: "Step 4: Upload Pattern to Machine",
- description:
- "Send your pattern to the embroidery machine to prepare for sewing.",
- items: [
- "Review the pattern preview to ensure it's positioned correctly",
- "Check the pattern size matches your hoop",
- 'Click "Upload to Machine" when ready',
- "Wait for the upload to complete (this may take a minute)",
- ],
- };
-
- case 5:
- // Check machine status for substates
- if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) {
- return {
- type: "warning" as const,
- title: "Machine Action Required",
- description: "The machine is ready to trace the pattern outline.",
- items: [
- "Press the button on your machine to confirm and start the mask trace",
- "Ensure the hoop is properly attached",
- "Make sure the needle area is clear",
- ],
- };
- }
- if (machineStatus === MachineStatus.MASK_TRACING) {
- return {
- type: "progress" as const,
- title: "Mask Trace In Progress",
- description:
- "The machine is tracing the pattern boundary. Please wait...",
- items: [
- "Watch the machine trace the outline",
- "Verify the pattern fits within your hoop",
- "Do not interrupt the machine",
- ],
- };
- }
- return {
- type: "info" as const,
- title: "Step 5: Start Mask Trace",
- description:
- "The mask trace helps the machine understand the pattern boundaries.",
- items: [
- 'Click "Start Mask Trace" button in the Sewing Progress section',
- "The machine will trace the pattern outline",
- "This ensures the hoop is positioned correctly",
- ],
- };
-
- case 6:
- return {
- type: "success" as const,
- title: "Step 6: Ready to Sew!",
- description: "The machine is ready to begin embroidering your pattern.",
- items: [
- "Verify your thread colors are correct",
- "Ensure the fabric is properly hooped",
- 'Click "Start Sewing" when ready',
- ],
- };
-
- case 7:
- // Check for substates
- if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) {
- return {
- type: "warning" as const,
- title: "Thread Change Required",
- description:
- "The machine needs a different thread color to continue.",
- items: [
- "Check the color blocks section to see which thread is needed",
- "Change to the correct thread color",
- "Press the button on your machine to resume sewing",
- ],
- };
- }
- if (
- machineStatus === MachineStatus.PAUSE ||
- machineStatus === MachineStatus.STOP ||
- machineStatus === MachineStatus.SEWING_INTERRUPTION
- ) {
- return {
- type: "warning" as const,
- title: "Sewing Paused",
- description: "The embroidery has been paused or interrupted.",
- items: [
- "Check if everything is okay with the machine",
- 'Click "Resume Sewing" when ready to continue',
- "The machine will pick up where it left off",
- ],
- };
- }
- return {
- type: "progress" as const,
- title: "Step 7: Sewing In Progress",
- description:
- "Your embroidery is being stitched. Monitor the progress below.",
- items: [
- "Watch the progress bar and current stitch count",
- "The machine will pause when a color change is needed",
- "Do not leave the machine unattended",
- ],
- };
-
- case 8:
- return {
- type: "success" as const,
- title: "Step 8: Embroidery Complete!",
- description: "Your embroidery is finished. Great work!",
- items: [
- "Remove the hoop from the machine",
- "Press the Accept button on the machine",
- "Carefully remove your finished embroidery",
- "Trim any jump stitches or loose threads",
- 'Click "Delete Pattern" to start a new project',
- ],
- };
-
- default:
- return null;
- }
-}
-
-function getCurrentStep(
- machineStatus: MachineStatus,
- isConnected: boolean,
- hasPattern: boolean,
- patternUploaded: boolean,
-): number {
- if (!isConnected) return 1;
-
- // Check if machine needs homing (Initial state)
- if (machineStatus === MachineStatus.Initial) return 2;
-
- if (!hasPattern) return 3;
- if (!patternUploaded) return 4;
-
- // After upload, determine step based on machine status
- switch (machineStatus) {
- case MachineStatus.IDLE:
- case MachineStatus.MASK_TRACE_LOCK_WAIT:
- case MachineStatus.MASK_TRACING:
- return 5;
-
- case MachineStatus.MASK_TRACE_COMPLETE:
- case MachineStatus.SEWING_WAIT:
- return 6;
-
- case MachineStatus.SEWING:
- case MachineStatus.COLOR_CHANGE_WAIT:
- case MachineStatus.PAUSE:
- case MachineStatus.STOP:
- case MachineStatus.SEWING_INTERRUPTION:
- return 7;
-
- case MachineStatus.SEWING_COMPLETE:
- return 8;
-
- default:
- return 5;
- }
-}
-
-export function WorkflowStepper() {
- // Machine store
- const { machineStatus, isConnected } = useMachineStore(
- useShallow((state) => ({
- machineStatus: state.machineStatus,
- isConnected: state.isConnected,
- })),
- );
-
- // Pattern store
- const { pesData } = usePatternStore(
- useShallow((state) => ({
- pesData: state.pesData,
- })),
- );
-
- // Derived state: pattern is uploaded if machine has pattern info
- const patternUploaded = usePatternUploaded();
- const hasPattern = pesData !== null;
- const currentStep = getCurrentStep(
- machineStatus,
- isConnected,
- hasPattern,
- patternUploaded,
- );
- const [showPopover, setShowPopover] = useState(false);
- const [popoverStep, setPopoverStep] = useState<number | null>(null);
- const popoverRef = useRef<HTMLDivElement>(null);
- const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
-
- // Close popover when clicking outside (exclude step circles)
- useClickOutside<HTMLDivElement>(popoverRef, () => setShowPopover(false), {
- enabled: showPopover,
- excludeRefs: [stepRefs],
- });
-
- const handleStepClick = (stepId: number) => {
- // Only allow clicking on current step or earlier completed steps
- if (stepId <= currentStep) {
- if (showPopover && popoverStep === stepId) {
- setShowPopover(false);
- setPopoverStep(null);
- } else {
- setPopoverStep(stepId);
- setShowPopover(true);
- }
- }
- };
-
- return (
- <div
- className="relative max-w-5xl mx-auto mt-2 lg:mt-4"
- role="navigation"
- aria-label="Workflow progress"
- >
- {/* Progress bar background */}
- <div
- className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-primary-400/20 dark:bg-primary-600/20 rounded-full"
- style={{ left: "16px", right: "16px" }}
- />
-
- {/* Progress bar fill */}
- <div
- className="absolute top-4 lg:top-5 left-0 h-0.5 lg:h-1 bg-gradient-to-r from-success-500 to-primary-500 dark:from-success-600 dark:to-primary-600 transition-all duration-500 rounded-full"
- style={{
- left: "16px",
- width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 16px)`,
- }}
- role="progressbar"
- aria-valuenow={currentStep}
- aria-valuemin={1}
- aria-valuemax={steps.length}
- aria-label={`Step ${currentStep} of ${steps.length}`}
- />
-
- {/* Steps */}
- <div className="flex justify-between relative">
- {steps.map((step) => {
- const isComplete = step.id < currentStep;
- const isCurrent = step.id === currentStep;
- const isUpcoming = step.id > currentStep;
-
- return (
- <div
- key={step.id}
- className="flex flex-col items-center"
- style={{ flex: 1 }}
- role="listitem"
- aria-current={isCurrent ? "step" : undefined}
- >
- {/* Step circle */}
- <div
- ref={(el) => {
- stepRefs.current[step.id] = el;
- }}
- onClick={() => handleStepClick(step.id)}
- className={`
- w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
- ${step.id <= currentStep ? "cursor-pointer hover:scale-110" : "cursor-not-allowed"}
- ${isComplete ? "bg-success-500 dark:bg-success-600 border-success-400 dark:border-success-500 text-white shadow-success-500/30 dark:shadow-success-600/30" : ""}
- ${isCurrent ? "bg-primary-600 dark:bg-primary-700 border-primary-500 dark:border-primary-600 text-white scale-105 lg:scale-110 shadow-primary-600/40 dark:shadow-primary-700/40 ring-2 ring-primary-300 dark:ring-primary-500 ring-offset-2 dark:ring-offset-gray-900" : ""}
- ${isUpcoming ? "bg-primary-700 dark:bg-primary-800 border-primary-500/30 dark:border-primary-600/30 text-primary-200/70 dark:text-primary-300/70" : ""}
- ${showPopover && popoverStep === step.id ? "ring-4 ring-white dark:ring-gray-800" : ""}
- `}
- aria-label={`${step.label}: ${isComplete ? "completed" : isCurrent ? "current" : "upcoming"}. Click for details.`}
- role="button"
- tabIndex={step.id <= currentStep ? 0 : -1}
- >
- {isComplete ? (
- <CheckCircleIcon
- className="w-5 h-5 lg:w-6 lg:h-6"
- aria-hidden="true"
- />
- ) : (
- step.id
- )}
- </div>
-
- {/* Step label */}
- <div className="mt-1 lg:mt-2 text-center">
- <div
- className={`text-xs font-semibold leading-tight ${
- isCurrent
- ? "text-white"
- : isComplete
- ? "text-success-200 dark:text-success-300"
- : "text-primary-300/70 dark:text-primary-400/70"
- }`}
- >
- {step.label}
- </div>
- </div>
- </div>
- );
- })}
- </div>
-
- {/* Popover */}
- {showPopover && popoverStep !== null && (
- <div
- ref={popoverRef}
- className="absolute top-full mt-4 left-1/2 transform -translate-x-1/2 w-full max-w-xl z-50 animate-fadeIn"
- role="dialog"
- aria-label="Step guidance"
- >
- {(() => {
- const content = getGuideContent(popoverStep, machineStatus);
- if (!content) return null;
-
- const colorClasses = {
- info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500",
- success:
- "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500",
- warning:
- "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500",
- error:
- "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500",
- progress:
- "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500",
- };
-
- const iconColorClasses = {
- info: "text-info-600 dark:text-info-400",
- success: "text-success-600 dark:text-success-400",
- warning: "text-warning-600 dark:text-warning-400",
- error: "text-danger-600 dark:text-danger-400",
- progress: "text-info-600 dark:text-info-400",
- };
-
- const textColorClasses = {
- info: "text-info-900 dark:text-info-200",
- success: "text-success-900 dark:text-success-200",
- warning: "text-warning-900 dark:text-warning-200",
- error: "text-danger-900 dark:text-danger-200",
- progress: "text-info-900 dark:text-info-200",
- };
-
- const descColorClasses = {
- info: "text-info-800 dark:text-info-300",
- success: "text-success-800 dark:text-success-300",
- warning: "text-warning-800 dark:text-warning-300",
- error: "text-danger-800 dark:text-danger-300",
- progress: "text-info-800 dark:text-info-300",
- };
-
- const listColorClasses = {
- info: "text-blue-700 dark:text-blue-300",
- success: "text-green-700 dark:text-green-300",
- warning: "text-yellow-700 dark:text-yellow-300",
- error: "text-red-700 dark:text-red-300",
- progress: "text-cyan-700 dark:text-cyan-300",
- };
-
- const Icon =
- content.type === "warning"
- ? ExclamationTriangleIcon
- : InformationCircleIcon;
-
- return (
- <div
- className={`${colorClasses[content.type]} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}
- >
- <div className="flex items-start gap-3">
- <Icon
- className={`w-6 h-6 ${iconColorClasses[content.type]} flex-shrink-0 mt-0.5`}
- />
- <div className="flex-1">
- <h3
- className={`text-base font-semibold ${textColorClasses[content.type]} mb-2`}
- >
- {content.title}
- </h3>
- <p
- className={`text-sm ${descColorClasses[content.type]} mb-3`}
- >
- {content.description}
- </p>
- {content.items && content.items.length > 0 && (
- <ul
- className={`list-disc list-inside text-sm ${listColorClasses[content.type]} space-y-1`}
- >
- {content.items.map((item, index) => (
- <li
- key={index}
- className="pl-2"
- dangerouslySetInnerHTML={{
- __html: item.replace(
- /\*\*(.*?)\*\*/g,
- "<strong>$1</strong>",
- ),
- }}
- />
- ))}
- </ul>
- )}
- </div>
- </div>
- </div>
- );
- })()}
- </div>
- )}
- </div>
- );
-}

diff --git a/src/components/WorkflowStepper/StepCircle.tsx b/src/components/WorkflowStepper/StepCircle.tsx
new file mode 100644
index 0000000..4c9ab83
--- /dev/null
+++ b/src/components/WorkflowStepper/StepCircle.tsx
@@ -0,0 +1,54 @@
+/**
+ * StepCircle Component
+ *
+ * Renders a circular step indicator with number or checkmark icon
+ */
+
+import { forwardRef } from "react";
+import { CheckCircleIcon } from "@heroicons/react/24/solid";
+
+export interface StepCircleProps {
+ stepId: number;
+ label: string;
+ isComplete: boolean;
+ isCurrent: boolean;
+ isUpcoming: boolean;
+ showPopover: boolean;
+ onClick: () => void;
+}
+
+export const StepCircle = forwardRef<HTMLDivElement, StepCircleProps>(
+ (
+ { stepId, label, isComplete, isCurrent, isUpcoming, showPopover, onClick },
+ ref,
+ ) => {
+ return (
+ <div
+ ref={ref}
+ onClick={onClick}
+ className={`
+ w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
+ ${stepId <= (isCurrent ? stepId : isComplete ? stepId : stepId - 1) ? "cursor-pointer hover:scale-110" : "cursor-not-allowed"}
+ ${isComplete ? "bg-success-500 dark:bg-success-600 border-success-400 dark:border-success-500 text-white shadow-success-500/30 dark:shadow-success-600/30" : ""}
+ ${isCurrent ? "bg-primary-600 dark:bg-primary-700 border-primary-500 dark:border-primary-600 text-white scale-105 lg:scale-110 shadow-primary-600/40 dark:shadow-primary-700/40 ring-2 ring-primary-300 dark:ring-primary-500 ring-offset-2 dark:ring-offset-gray-900" : ""}
+ ${isUpcoming ? "bg-primary-700 dark:bg-primary-800 border-primary-500/30 dark:border-primary-600/30 text-primary-200/70 dark:text-primary-300/70" : ""}
+ ${showPopover ? "ring-4 ring-white dark:ring-gray-800" : ""}
+ `}
+ aria-label={`${label}: ${isComplete ? "completed" : isCurrent ? "current" : "upcoming"}. Click for details.`}
+ role="button"
+ tabIndex={isComplete || isCurrent ? 0 : -1}
+ >
+ {isComplete ? (
+ <CheckCircleIcon
+ className="w-5 h-5 lg:w-6 lg:h-6"
+ aria-hidden="true"
+ />
+ ) : (
+ stepId
+ )}
+ </div>
+ );
+ },
+);
+
+StepCircle.displayName = "StepCircle";

diff --git a/src/components/WorkflowStepper/StepLabel.tsx b/src/components/WorkflowStepper/StepLabel.tsx
new file mode 100644
index 0000000..617b676
--- /dev/null
+++ b/src/components/WorkflowStepper/StepLabel.tsx
@@ -0,0 +1,29 @@
+/**
+ * StepLabel Component
+ *
+ * Renders the text label below each step circle
+ */
+
+export interface StepLabelProps {
+ label: string;
+ isCurrent: boolean;
+ isComplete: boolean;
+}
+
+export function StepLabel({ label, isCurrent, isComplete }: StepLabelProps) {
+ return (
+ <div className="mt-1 lg:mt-2 text-center">
+ <div
+ className={`text-xs font-semibold leading-tight ${
+ isCurrent
+ ? "text-white"
+ : isComplete
+ ? "text-success-200 dark:text-success-300"
+ : "text-primary-300/70 dark:text-primary-400/70"
+ }`}
+ >
+ {label}
+ </div>
+ </div>
+ );
+}

diff --git a/src/components/WorkflowStepper/StepPopover.tsx b/src/components/WorkflowStepper/StepPopover.tsx
new file mode 100644
index 0000000..711fe8a
--- /dev/null
+++ b/src/components/WorkflowStepper/StepPopover.tsx
@@ -0,0 +1,123 @@
+/**
+ * StepPopover Component
+ *
+ * Renders the guidance popover with dynamic content based on step and machine status
+ */
+
+import { forwardRef } from "react";
+import {
+ InformationCircleIcon,
+ ExclamationTriangleIcon,
+} from "@heroicons/react/24/solid";
+import { MachineStatus } from "../../types/machine";
+import { getGuideContent } from "../../utils/workflowGuideContent";
+
+export interface StepPopoverProps {
+ stepId: number;
+ machineStatus: MachineStatus;
+}
+
+export const StepPopover = forwardRef<HTMLDivElement, StepPopoverProps>(
+ ({ stepId, machineStatus }, ref) => {
+ const content = getGuideContent(stepId, machineStatus);
+ if (!content) return null;
+
+ const colorClasses = {
+ info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500",
+ success:
+ "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500",
+ warning:
+ "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500",
+ error:
+ "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500",
+ progress:
+ "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500",
+ };
+
+ const iconColorClasses = {
+ info: "text-info-600 dark:text-info-400",
+ success: "text-success-600 dark:text-success-400",
+ warning: "text-warning-600 dark:text-warning-400",
+ error: "text-danger-600 dark:text-danger-400",
+ progress: "text-info-600 dark:text-info-400",
+ };
+
+ const textColorClasses = {
+ info: "text-info-900 dark:text-info-200",
+ success: "text-success-900 dark:text-success-200",
+ warning: "text-warning-900 dark:text-warning-200",
+ error: "text-danger-900 dark:text-danger-200",
+ progress: "text-info-900 dark:text-info-200",
+ };
+
+ const descColorClasses = {
+ info: "text-info-800 dark:text-info-300",
+ success: "text-success-800 dark:text-success-300",
+ warning: "text-warning-800 dark:text-warning-300",
+ error: "text-danger-800 dark:text-danger-300",
+ progress: "text-info-800 dark:text-info-300",
+ };
+
+ const listColorClasses = {
+ info: "text-blue-700 dark:text-blue-300",
+ success: "text-green-700 dark:text-green-300",
+ warning: "text-yellow-700 dark:text-yellow-300",
+ error: "text-red-700 dark:text-red-300",
+ progress: "text-cyan-700 dark:text-cyan-300",
+ };
+
+ const Icon =
+ content.type === "warning"
+ ? ExclamationTriangleIcon
+ : InformationCircleIcon;
+
+ return (
+ <div
+ ref={ref}
+ className="absolute top-full mt-4 left-1/2 transform -translate-x-1/2 w-full max-w-xl z-50 animate-fadeIn"
+ role="dialog"
+ aria-label="Step guidance"
+ >
+ <div
+ className={`${colorClasses[content.type]} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}
+ >
+ <div className="flex items-start gap-3">
+ <Icon
+ className={`w-6 h-6 ${iconColorClasses[content.type]} flex-shrink-0 mt-0.5`}
+ />
+ <div className="flex-1">
+ <h3
+ className={`text-base font-semibold ${textColorClasses[content.type]} mb-2`}
+ >
+ {content.title}
+ </h3>
+ <p className={`text-sm ${descColorClasses[content.type]} mb-3`}>
+ {content.description}
+ </p>
+ {content.items && content.items.length > 0 && (
+ <ul
+ className={`list-disc list-inside text-sm ${listColorClasses[content.type]} space-y-1`}
+ >
+ {content.items.map((item, index) => (
+ <li
+ key={index}
+ className="pl-2"
+ dangerouslySetInnerHTML={{
+ __html: item.replace(
+ /\*\*(.*?)\*\*/g,
+ "<strong>$1</strong>",
+ ),
+ }}
+ />
+ ))}
+ </ul>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ },
+);
+
+StepPopover.displayName = "StepPopover";

diff --git a/src/components/WorkflowStepper/WorkflowStepper.tsx b/src/components/WorkflowStepper/WorkflowStepper.tsx
new file mode 100644
index 0000000..c065ced
--- /dev/null
+++ b/src/components/WorkflowStepper/WorkflowStepper.tsx
@@ -0,0 +1,141 @@
+/**
+ * WorkflowStepper Component
+ *
+ * Displays the 8-step embroidery workflow with progress tracking and contextual guidance
+ */
+
+import { useState, useRef } from "react";
+import { useClickOutside } from "@/hooks";
+import { useShallow } from "zustand/react/shallow";
+import { useMachineStore, usePatternUploaded } from "../../stores/useMachineStore";
+import { usePatternStore } from "../../stores/usePatternStore";
+import { WORKFLOW_STEPS } from "../../constants/workflowSteps";
+import { getCurrentStep } from "../../utils/workflowStepCalculation";
+import { StepCircle } from "./StepCircle";
+import { StepLabel } from "./StepLabel";
+import { StepPopover } from "./StepPopover";
+
+export function WorkflowStepper() {
+ // Machine store
+ const { machineStatus, isConnected } = useMachineStore(
+ useShallow((state) => ({
+ machineStatus: state.machineStatus,
+ isConnected: state.isConnected,
+ })),
+ );
+
+ // Pattern store
+ const { pesData } = usePatternStore(
+ useShallow((state) => ({
+ pesData: state.pesData,
+ })),
+ );
+
+ // Derived state: pattern is uploaded if machine has pattern info
+ const patternUploaded = usePatternUploaded();
+ const hasPattern = pesData !== null;
+ const currentStep = getCurrentStep(
+ machineStatus,
+ isConnected,
+ hasPattern,
+ patternUploaded,
+ );
+ const [showPopover, setShowPopover] = useState(false);
+ const [popoverStep, setPopoverStep] = useState<number | null>(null);
+ const popoverRef = useRef<HTMLDivElement>(null);
+ const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
+
+ // Close popover when clicking outside (exclude step circles)
+ useClickOutside<HTMLDivElement>(popoverRef, () => setShowPopover(false), {
+ enabled: showPopover,
+ excludeRefs: [stepRefs],
+ });
+
+ const handleStepClick = (stepId: number) => {
+ // Only allow clicking on current step or earlier completed steps
+ if (stepId <= currentStep) {
+ if (showPopover && popoverStep === stepId) {
+ setShowPopover(false);
+ setPopoverStep(null);
+ } else {
+ setPopoverStep(stepId);
+ setShowPopover(true);
+ }
+ }
+ };
+
+ return (
+ <div
+ className="relative max-w-5xl mx-auto mt-2 lg:mt-4"
+ role="navigation"
+ aria-label="Workflow progress"
+ >
+ {/* Progress bar background */}
+ <div
+ className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-primary-400/20 dark:bg-primary-600/20 rounded-full"
+ style={{ left: "16px", right: "16px" }}
+ />
+
+ {/* Progress bar fill */}
+ <div
+ className="absolute top-4 lg:top-5 left-0 h-0.5 lg:h-1 bg-gradient-to-r from-success-500 to-primary-500 dark:from-success-600 dark:to-primary-600 transition-all duration-500 rounded-full"
+ style={{
+ left: "16px",
+ width: `calc(${((currentStep - 1) / (WORKFLOW_STEPS.length - 1)) * 100}% - 16px)`,
+ }}
+ role="progressbar"
+ aria-valuenow={currentStep}
+ aria-valuemin={1}
+ aria-valuemax={WORKFLOW_STEPS.length}
+ aria-label={`Step ${currentStep} of ${WORKFLOW_STEPS.length}`}
+ />
+
+ {/* Steps */}
+ <div className="flex justify-between relative">
+ {WORKFLOW_STEPS.map((step) => {
+ const isComplete = step.id < currentStep;
+ const isCurrent = step.id === currentStep;
+ const isUpcoming = step.id > currentStep;
+
+ return (
+ <div
+ key={step.id}
+ className="flex flex-col items-center"
+ style={{ flex: 1 }}
+ role="listitem"
+ aria-current={isCurrent ? "step" : undefined}
+ >
+ <StepCircle
+ ref={(el: HTMLDivElement | null) => {
+ stepRefs.current[step.id] = el;
+ }}
+ stepId={step.id}
+ label={step.label}
+ isComplete={isComplete}
+ isCurrent={isCurrent}
+ isUpcoming={isUpcoming}
+ showPopover={showPopover && popoverStep === step.id}
+ onClick={() => handleStepClick(step.id)}
+ />
+
+ <StepLabel
+ label={step.label}
+ isCurrent={isCurrent}
+ isComplete={isComplete}
+ />
+ </div>
+ );
+ })}
+ </div>
+
+ {/* Popover */}
+ {showPopover && popoverStep !== null && (
+ <StepPopover
+ ref={popoverRef}
+ stepId={popoverStep}
+ machineStatus={machineStatus}
+ />
+ )}
+ </div>
+ );
+}

diff --git a/src/components/WorkflowStepper/index.ts b/src/components/WorkflowStepper/index.ts
new file mode 100644
index 0000000..e1b594e
--- /dev/null
+++ b/src/components/WorkflowStepper/index.ts
@@ -0,0 +1,5 @@
+/**
+ * WorkflowStepper component barrel export
+ */
+
+export { WorkflowStepper } from "./WorkflowStepper";

diff --git a/src/constants/workflowSteps.ts b/src/constants/workflowSteps.ts
new file mode 100644
index 0000000..63b7864
--- /dev/null
+++ b/src/constants/workflowSteps.ts
@@ -0,0 +1,20 @@
+/**
+ * Workflow step definitions for the embroidery process
+ */
+
+export interface WorkflowStep {
+ id: number;
+ label: string;
+ description: string;
+}
+
+export const WORKFLOW_STEPS: readonly WorkflowStep[] = [
+ { id: 1, label: "Connect", description: "Connect to machine" },
+ { id: 2, label: "Home Machine", description: "Initialize hoop position" },
+ { id: 3, label: "Load Pattern", description: "Choose PES file" },
+ { id: 4, label: "Upload", description: "Upload to machine" },
+ { id: 5, label: "Mask Trace", description: "Trace pattern area" },
+ { id: 6, label: "Start Sewing", description: "Begin embroidery" },
+ { id: 7, label: "Monitor", description: "Watch progress" },
+ { id: 8, label: "Complete", description: "Finish and remove" },
+] as const;

diff --git a/src/utils/workflowGuideContent.ts b/src/utils/workflowGuideContent.ts
new file mode 100644
index 0000000..e0d1b70
--- /dev/null
+++ b/src/utils/workflowGuideContent.ts
@@ -0,0 +1,195 @@
+/**
+ * Workflow step guide content
+ *
+ * Provides contextual guidance for each workflow step based on machine state
+ */
+
+import { MachineStatus } from "../types/machine";
+
+export interface GuideContent {
+ type: "info" | "warning" | "success" | "error" | "progress";
+ title: string;
+ description: string;
+ items: string[];
+}
+
+/**
+ * Get guide content for a specific workflow step
+ *
+ * @param stepId - The workflow step ID (1-8)
+ * @param machineStatus - Current machine status for dynamic content
+ * @returns Guide content with type, title, description, and items
+ */
+export function getGuideContent(
+ stepId: number,
+ machineStatus: MachineStatus,
+): GuideContent | null {
+ switch (stepId) {
+ case 1:
+ return {
+ type: "info",
+ title: "Step 1: Connect to Machine",
+ description:
+ "To get started, connect to your Brother embroidery machine via Bluetooth.",
+ items: [
+ "Make sure your machine is powered on",
+ "Enable Bluetooth on your machine",
+ 'Click the "Connect to Machine" button below',
+ ],
+ };
+
+ case 2:
+ return {
+ type: "info",
+ title: "Step 2: Home Machine",
+ description:
+ "The hoop needs to be removed and an initial homing procedure must be performed.",
+ items: [
+ "Remove the embroidery hoop from the machine completely",
+ "Press the Accept button on the machine",
+ "Wait for the machine to complete its initialization (homing)",
+ "Once initialization is complete, reattach the hoop",
+ "The machine should now recognize the hoop correctly",
+ ],
+ };
+
+ case 3:
+ return {
+ type: "info",
+ title: "Step 3: Load Your Pattern",
+ description:
+ "Choose a PES embroidery file from your computer to preview and upload.",
+ items: [
+ 'Click "Choose PES File" in the Pattern File section',
+ "Select your embroidery design (.pes file)",
+ "Review the pattern preview on the right",
+ "You can drag the pattern to adjust its position",
+ ],
+ };
+
+ case 4:
+ return {
+ type: "info",
+ title: "Step 4: Upload Pattern to Machine",
+ description:
+ "Send your pattern to the embroidery machine to prepare for sewing.",
+ items: [
+ "Review the pattern preview to ensure it's positioned correctly",
+ "Check the pattern size matches your hoop",
+ 'Click "Upload to Machine" when ready',
+ "Wait for the upload to complete (this may take a minute)",
+ ],
+ };
+
+ case 5:
+ // Check machine status for substates
+ if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) {
+ return {
+ type: "warning",
+ title: "Machine Action Required",
+ description: "The machine is ready to trace the pattern outline.",
+ items: [
+ "Press the button on your machine to confirm and start the mask trace",
+ "Ensure the hoop is properly attached",
+ "Make sure the needle area is clear",
+ ],
+ };
+ }
+ if (machineStatus === MachineStatus.MASK_TRACING) {
+ return {
+ type: "progress",
+ title: "Mask Trace In Progress",
+ description:
+ "The machine is tracing the pattern boundary. Please wait...",
+ items: [
+ "Watch the machine trace the outline",
+ "Verify the pattern fits within your hoop",
+ "Do not interrupt the machine",
+ ],
+ };
+ }
+ return {
+ type: "info",
+ title: "Step 5: Start Mask Trace",
+ description:
+ "The mask trace helps the machine understand the pattern boundaries.",
+ items: [
+ 'Click "Start Mask Trace" button in the Sewing Progress section',
+ "The machine will trace the pattern outline",
+ "This ensures the hoop is positioned correctly",
+ ],
+ };
+
+ case 6:
+ return {
+ type: "success",
+ title: "Step 6: Ready to Sew!",
+ description: "The machine is ready to begin embroidering your pattern.",
+ items: [
+ "Verify your thread colors are correct",
+ "Ensure the fabric is properly hooped",
+ 'Click "Start Sewing" when ready',
+ ],
+ };
+
+ case 7:
+ // Check for substates
+ if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) {
+ return {
+ type: "warning",
+ title: "Thread Change Required",
+ description:
+ "The machine needs a different thread color to continue.",
+ items: [
+ "Check the color blocks section to see which thread is needed",
+ "Change to the correct thread color",
+ "Press the button on your machine to resume sewing",
+ ],
+ };
+ }
+ if (
+ machineStatus === MachineStatus.PAUSE ||
+ machineStatus === MachineStatus.STOP ||
+ machineStatus === MachineStatus.SEWING_INTERRUPTION
+ ) {
+ return {
+ type: "warning",
+ title: "Sewing Paused",
+ description: "The embroidery has been paused or interrupted.",
+ items: [
+ "Check if everything is okay with the machine",
+ 'Click "Resume Sewing" when ready to continue',
+ "The machine will pick up where it left off",
+ ],
+ };
+ }
+ return {
+ type: "progress",
+ title: "Step 7: Sewing In Progress",
+ description:
+ "Your embroidery is being stitched. Monitor the progress below.",
+ items: [
+ "Watch the progress bar and current stitch count",
+ "The machine will pause when a color change is needed",
+ "Do not leave the machine unattended",
+ ],
+ };
+
+ case 8:
+ return {
+ type: "success",
+ title: "Step 8: Embroidery Complete!",
+ description: "Your embroidery is finished. Great work!",
+ items: [
+ "Remove the hoop from the machine",
+ "Press the Accept button on the machine",
+ "Carefully remove your finished embroidery",
+ "Trim any jump stitches or loose threads",
+ 'Click "Delete Pattern" to start a new project',
+ ],
+ };
+
+ default:
+ return null;
+ }
+}

diff --git a/src/utils/workflowStepCalculation.ts b/src/utils/workflowStepCalculation.ts
new file mode 100644
index 0000000..42e9c6c
--- /dev/null
+++ b/src/utils/workflowStepCalculation.ts
@@ -0,0 +1,56 @@
+/**
+ * Workflow step calculation utilities
+ *
+ * Determines the current workflow step based on machine state and pattern status
+ */
+
+import { MachineStatus } from "../types/machine";
+
+/**
+ * Calculate the current workflow step based on machine state
+ *
+ * @param machineStatus - Current machine status
+ * @param isConnected - Whether machine is connected
+ * @param hasPattern - Whether a pattern is loaded
+ * @param patternUploaded - Whether pattern has been uploaded to machine
+ * @returns Current step number (1-8)
+ */
+export function getCurrentStep(
+ machineStatus: MachineStatus,
+ isConnected: boolean,
+ hasPattern: boolean,
+ patternUploaded: boolean,
+): number {
+ if (!isConnected) return 1;
+
+ // Check if machine needs homing (Initial state)
+ if (machineStatus === MachineStatus.Initial) return 2;
+
+ if (!hasPattern) return 3;
+ if (!patternUploaded) return 4;
+
+ // After upload, determine step based on machine status
+ switch (machineStatus) {
+ case MachineStatus.IDLE:
+ case MachineStatus.MASK_TRACE_LOCK_WAIT:
+ case MachineStatus.MASK_TRACING:
+ return 5;
+
+ case MachineStatus.MASK_TRACE_COMPLETE:
+ case MachineStatus.SEWING_WAIT:
+ return 6;
+
+ case MachineStatus.SEWING:
+ case MachineStatus.COLOR_CHANGE_WAIT:
+ case MachineStatus.PAUSE:
+ case MachineStatus.STOP:
+ case MachineStatus.SEWING_INTERRUPTION:
+ return 7;
+
+ case MachineStatus.SEWING_COMPLETE:
+ return 8;
+
+ default:
+ return 5;
+ }
+}

Served by rngit 1.3.3 - Generated in 0.07s